sartUP — Contenitore Admin “menu–oriented” (Blade + Auth nativa + Spatie ruoli)

sartUP — Contenitore Admin “menu–oriented” (Blade + Auth nativa + Spatie ruoli)

Obiettivo: tutto in Markdown per Cursor: struttura cartelle coerente con il menù, controllers/models/views/helpers organizzati per sezioni del menù, con Service Menu (Super Admin) in testa e Configurazione Menù come prima voce.

---

0) Filosofia di struttura (menu → cartelle)

L’architettura del codice ricalca la gerarchia del menù:
  • Topbar (Livello 1) = macro–sezione (es. Dashboard, Industria 4.0, Impostazioni, Servizio).
  • Sidebar (Livello 2/3/4) = sottosezioni/nodi figli.
  • Il codice va in sottoCartelle parlanti che ripetono i nomi delle sezioni.
  • Le rotte sono raggruppate per sezione, i controller sono in namespace dedicati, le view Blade seguono la stessa tassonomia.
  • ---

    1) Struttura cartelle (proposta)

    `` app/ Http/ Controllers/ Admin/ Dashboard/ DashboardController.php I40/ HomeController.php Machines/ MachinesController.php Systems/ # "Servizio" (super-admin only) Menu/ MenuController.php Users/ UsersController.php Roles/ RolesController.php Middleware/ EnsureActiveRole.php Models/ Menu.php MenuItem.php Policies/ MenuPolicy.php app/ Services/ MenuService.php resources/ views/ layouts/ admin.blade.php auth.blade.php auth/ login.blade.php select-role.blade.php forgot.blade.php reset.blade.php admin/ dashboard/index.blade.php i40/home.blade.php i40/machines/connected.blade.php systems/ # Servizio (Super Admin) index.blade.php menu/ index.blade.php # lista voci create.blade.php edit.blade.php routes/ admin.php auth.php database/ migrations/ 2025_xx_xx_create_menus_table.php 2025_xx_xx_create_menu_items_table.php seeders/ RolesSeeder.php SuperAdminSeeder.php MenuSeeder.php `

    > Nota: SystemsServizio. La voce di topbar “Servizio” sarà visibile solo al ruolo super-admin.

    ---

    2) Rotte (separate per ambito)

    2.1 routes/auth.php

    `php <?php

    use Illuminate\Support\Facades\Route; use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\Auth\ForgotPasswordController; use App\Http\Controllers\Auth\ResetPasswordController; use App\Http\Controllers\Auth\RoleSelectorController;

    Route::middleware('guest')->group(function () { Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login'); Route::post('/login', [LoginController::class, 'login'])->name('login.post'); Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request'); Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email'); Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset'); Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update'); });

    Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');

    Route::middleware('auth')->group(function () { Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select'); Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set'); }); `

    2.2 routes/admin.php

    `php <?php

    use Illuminate\Support\Facades\Route;

    Route::middleware(['auth','active.role'])->prefix('admin')->name('admin.')->group(function () {

    // Dashboard Route::get('/', [\App\Http\Controllers\Admin\Dashboard\DashboardController::class,'index']) ->name('dashboard');

    // Industria 4.0 Route::prefix('i40')->name('i40.')->group(function() { Route::get('/', [\App\Http\Controllers\Admin\I40\HomeController::class,'index'])->name('home'); Route::get('/machines/connected', [\App\Http\Controllers\Admin\I40\Machines\MachinesController::class,'connected']) ->name('machines.connected'); });

    // Servizio (Super Admin menu) Route::prefix('systems')->name('systems.')->middleware('role:super-admin')->group(function () { Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('home');

    // Configurazione menù (prima voce) Route::prefix('menu')->name('menu.')->group(function () { Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('index'); Route::get('/create', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'create'])->name('create'); Route::post('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'store'])->name('store'); Route::get('/{item}/edit', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'edit'])->name('edit'); Route::put('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'update'])->name('update'); Route::delete('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'destroy'])->name('destroy'); Route::post('/reorder', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'reorder'])->name('reorder'); }); });

    }); `

    ---

    3) Controller — esempi orientati a cartelle

    3.1 Dashboard

    `php <?php namespace App\Http\Controllers\Admin\Dashboard;

    use App\Http\Controllers\Controller;

    class DashboardController extends Controller { public function index() { return view('admin.dashboard.index'); } } `

    3.2 I4.0 → Machines

    `php <?php namespace App\Http\Controllers\Admin\I40\Machines;

    use App\Http\Controllers\Controller;

    class MachinesController extends Controller { public function connected() { return view('admin.i40.machines.connected'); } } `

    3.3 Systems → Menu (super-admin)

    `php <?php namespace App\Http\Controllers\Admin\Systems\Menu;

    use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Models\Menu; use App\Models\MenuItem;

    class MenuController extends Controller { public function index() { $menu = Menu::where('name','admin_main')->first(); $items = $menu ? $menu->items()->orderBy('order_index')->get() : collect(); return view('admin.systems.menu.index', compact('menu','items')); }

    public function create() { $menu = Menu::where('name','admin_main')->first(); $parents = $menu?->items()->orderBy('label')->get(); return view('admin.systems.menu.create', compact('menu','parents')); }

    public function store(Request $r) { $r->validate([ 'label'=>'required|string|max:100', 'parent_id'=>'nullable|integer', 'route_name'=>'nullable|string|max:120', 'url'=>'nullable|url', 'order_index'=>'nullable|integer', ]); $menu = Menu::firstOrCreate(['name'=>'admin_main']); MenuItem::create([ 'menu_id'=>$menu->id, 'parent_id'=>$r->parent_id, 'label'=>$r->label, 'route_name'=>$r->route_name ?: null, 'url'=>$r->url ?: null, 'icon'=>$r->icon, 'description'=>$r->description, 'order_index'=>$r->order_index ?? 0, 'is_visible'=>$r->boolean('is_visible', true), 'required_roles'=>$r->filled('required_roles') ? array_values(array_filter(array_map('trim', explode(',', $r->required_roles)))) : null, 'required_permissions'=>$r->filled('required_permissions') ? array_values(array_filter(array_map('trim', explode(',', $r->required_permissions)))) : null, ]); return redirect()->route('admin.systems.menu.index')->with('ok','Voce aggiunta'); }

    public function edit(MenuItem $item) { $menu = Menu::where('name','admin_main')->first(); $parents = $menu?->items()->where('id','!=',$item->id)->orderBy('label')->get(); return view('admin.systems.menu.edit', compact('item','parents')); }

    public function update(Request $r, MenuItem $item) { $r->validate([ 'label'=>'required|string|max:100', 'parent_id'=>'nullable|integer', 'route_name'=>'nullable|string|max:120', 'url'=>'nullable|url', 'order_index'=>'nullable|integer', ]); $item->update([ 'parent_id'=>$r->parent_id, 'label'=>$r->label, 'route_name'=>$r->route_name ?: null, 'url'=>$r->url ?: null, 'icon'=>$r->icon, 'description'=>$r->description, 'order_index'=>$r->order_index ?? 0, 'is_visible'=>$r->boolean('is_visible', true), 'required_roles'=>$r->filled('required_roles') ? array_values(array_filter(array_map('trim', explode(',', $r->required_roles)))) : null, 'required_permissions'=>$r->filled('required_permissions') ? array_values(array_filter(array_map('trim', explode(',', $r->required_permissions)))) : null, ]); return redirect()->route('admin.systems.menu.index')->with('ok','Voce aggiornata'); }

    public function destroy(MenuItem $item) { $item->delete(); return back()->with('ok','Voce rimossa'); }

    public function reorder(Request $r) { foreach ($r->input('items',[]) as $row) { \App\Models\MenuItem::whereKey($row['id'])->update(['order_index'=>$row['order_index']]); } return response()->json(['ok'=>true]); } } `

    ---

    4) Models & Policy

    4.1 Models

    `php <?php // app/Models/Menu.php namespace App\Models; use Illuminate\Database\Eloquent\Model;

    class Menu extends Model { protected $fillable = ['name','description','is_active']; public function items() { return $this->hasMany(MenuItem::class); } } `

    `php <?php // app/Models/MenuItem.php namespace App\Models; use Illuminate\Database\Eloquent\Model;

    class MenuItem extends Model { protected $fillable = [ 'menu_id','parent_id','label','route_name','url','icon','description', 'order_index','is_visible','required_roles','required_permissions' ]; protected $casts = ['required_roles'=>'array','required_permissions'=>'array']; public function parent(){ return $this->belongsTo(MenuItem::class,'parent_id'); } public function children(){ return $this->hasMany(MenuItem::class,'parent_id'); } public function menu(){ return $this->belongsTo(Menu::class); } } `

    4.2 Policy (opzionale per gestione menu)

    `php <?php // app/Policies/MenuPolicy.php namespace App\Policies; use App\Models\User; use App\Models\MenuItem;

    class MenuPolicy { public function manage(User $user) { return $user->hasRole('super-admin'); } public function view(User $user, MenuItem $item) { if ($user->hasRole('super-admin')) return true; $activeRole = session('active_role'); if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false; if ($item->required_permissions) { foreach ($item->required_permissions as $perm) { if (!$user->can($perm)) return false; } } return true; } } `

    ---

    5) Service & Helpers (menu rendering/attivi)

    5.1 Service

    `php <?php // app/Services/MenuService.php namespace App\Services; use App\Models\Menu; use App\Models\MenuItem; use App\Models\User;

    class MenuService { public function forUserMenu(string $menuName, ?User $user): array { $menu = Menu::where('name',$menuName)->first(); if (!$menu) return []; $items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id'); $activeRole = session('active_role');

    $filter = function(MenuItem $item) use ($user,$activeRole) { if (!$item->is_visible) return false; if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false; if ($item->required_permissions) { foreach ($item->required_permissions as $perm) { if (!$user || !$user->can($perm)) return false; } } return true; };

    $build = function($parentId) use (&$build, $items, $filter) { return ($items[$parentId] ?? collect())->filter($filter)->map(function($i) use (&$build) { return [ 'id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'icon'=>$i->icon, 'children'=>$build($i->id)->values()->all() ]; }); };

    return $build(null)->values()->all(); } } `

    5.2 Helper Blade (attivo)

    `php <?php // app/helpers.php if (!function_exists('menu_is_active')) { function menu_is_active(?string $routeName): bool { if (!$routeName) return false; return request()->routeIs($routeName) || request()->routeIs($routeName.'.*'); } } `

    composer.json (autoload): `json "autoload": { "psr-4": { "App\\": "app/" }, "files": [ "app/helpers.php" ] } `

    ---

    6) Views Blade (layout + sezioni)

    6.1 Layout admin

    `blade {{-- resources/views/layouts/admin.blade.php --}} @php($menu = app(\App\Services\MenuService::class)->forUserMenu('admin_main', auth()->user())) <!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>@yield('title','sartUP Admin')</title> @vite(['resources/css/app.css','resources/js/app.js']) </head> <body class="min-h-screen bg-gray-50"> <header class="h-14 shadow flex items-center px-4 bg-white"> <nav class="flex gap-4"> @foreach($menu as $item) <a class="font-medium {{ menu_is_active($item['route_name']) ? 'text-blue-600' : '' }}" href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}"> {{ $item['label'] }} </a> @endforeach @role('super-admin') <a class="font-medium {{ request()->is('admin/systems*') ? 'text-blue-600' : '' }}" href="{{ route('admin.systems.menu.index') }}">Servizio</a> @endrole </nav> <div class="ml-auto flex items-center gap-2"> @if(session('active_role')) <span class="text-xs px-2 py-1 bg-gray-200 rounded">Ruolo: {{ session('active_role') }}</span> @endif <form method="POST" action="{{ route('logout') }}">@csrf<button>Logout</button></form> </div> </header> <div class="flex"> <aside class="w-64 bg-white border-r min-h-[calc(100vh-3.5rem)] p-3"> @php($first = $menu[0] ?? null) @if($first && count($first['children'])) @foreach($first['children'] as $child) <div class="mb-3"> <div class="font-semibold">{{ $child['label'] }}</div> @if(count($child['children'])) <ul class="ml-3 list-disc"> @foreach($child['children'] as $sub) <li> <a href="{{ $sub['route_name'] ? route($sub['route_name']) : ($sub['url'] ?? '#') }}"> {{ $sub['label'] }} </a> </li> @endforeach </ul> @endif </div> @endforeach @endif </aside> <main class="flex-1 p-6"> @if(session('ok')) <div class="mb-4 p-2 bg-green-100">{{ session('ok') }}</div> @endif @yield('content') </main> </div> </body> </html> `

    6.2 Views “Servizio → Configurazione Menù”

    `blade {{-- resources/views/admin/systems/index.blade.php --}} @extends('layouts.admin') @section('title','Servizio') @section('content') <h1 class="text-xl font-semibold mb-4">Servizio</h1> <ul class="list-disc ml-6"> <li><a href="{{ route('admin.systems.menu.index') }}">Configurazione menù</a></li> </ul> @endsection `

    `blade {{-- resources/views/admin/systems/menu/index.blade.php --}} @extends('layouts.admin') @section('title','Configurazione menù') @section('content') <h1 class="text-xl font-semibold mb-4">Configurazione menù</h1>

    <a href="{{ route('admin.systems.menu.create') }}" class="underline">Nuova voce</a>

    <table class="min-w-full bg-white shadow border mt-4"> <thead><tr> <th class="p-2 text-left">Label</th> <th class="p-2 text-left">Parent</th> <th class="p-2 text-left">Route</th> <th class="p-2 text-left">URL</th> <th class="p-2 text-left">Roles</th> <th class="p-2 text-left">Ordine</th> <th class="p-2"></th> </tr></thead> <tbody> @forelse($items as $i) <tr> <td class="p-2">{{ $i->label }}</td> <td class="p-2">{{ $i->parent?->label ?? '—' }}</td> <td class="p-2">{{ $i->route_name ?? '—' }}</td> <td class="p-2">{{ $i->url ?? '—' }}</td> <td class="p-2">{{ $i->required_roles ? implode(',', $i->required_roles) : '—' }}</td> <td class="p-2">{{ $i->order_index }}</td> <td class="p-2"> <a class="underline" href="{{ route('admin.systems.menu.edit',$i) }}">Modifica</a> <form method="POST" action="{{ route('admin.systems.menu.destroy',$i) }}" class="inline"> @csrf @method('DELETE') <button class="underline text-red-600" onclick="return confirm('Eliminare?')">Elimina</button> </form> </td> </tr> @empty <tr><td class="p-2" colspan="7">Nessuna voce ancora.</td></tr> @endforelse </tbody> </table> @endsection `

    `blade {{-- resources/views/admin/systems/menu/create.blade.php --}} @extends('layouts.admin') @section('title','Nuova voce menù') @section('content') <h1 class="text-xl font-semibold mb-4">Nuova voce</h1> <form method="POST" action="{{ route('admin.systems.menu.store') }}"> @csrf <div class="mb-2"> <label>Label</label> <input class="border p-1 w-full" name="label" required> </div> <div class="mb-2"> <label>Parent</label> <select class="border p-1 w-full" name="parent_id"> <option value="">—</option> @foreach($parents as $p) <option value="{{ $p->id }}">{{ $p->label }}</option> @endforeach </select> </div> <div class="mb-2"> <label>Route name</label> <input class="border p-1 w-full" name="route_name"> </div> <div class="mb-2"> <label>URL</label> <input class="border p-1 w-full" name="url" placeholder="http(s)://…"> </div> <div class="mb-2"> <label>Icon</label> <input class="border p-1 w-full" name="icon"> </div> <div class="mb-2"> <label>Descrizione</label> <input class="border p-1 w-full" name="description"> </div> <div class="mb-2"> <label>Ordine</label> <input class="border p-1 w-full" name="order_index" type="number" value="0"> </div> <div class="mb-2"> <label>Ruoli richiesti (comma separated)</label> <input class="border p-1 w-full" name="required_roles" placeholder="admin,maintenance"> </div> <div class="mb-2"> <label>Visibile?</label> <input type="checkbox" name="is_visible" checked> </div> <button class="px-3 py-1 bg-blue-600 text-white rounded">Salva</button> </form> @endsection `

    `blade {{-- resources/views/admin/systems/menu/edit.blade.php --}} @extends('layouts.admin') @section('title','Modifica voce menù') @section('content') <h1 class="text-xl font-semibold mb-4">Modifica voce</h1> <form method="POST" action="{{ route('admin.systems.menu.update',$item) }}"> @csrf @method('PUT') <div class="mb-2"> <label>Label</label> <input class="border p-1 w-full" name="label" required value="{{ old('label',$item->label) }}"> </div> <div class="mb-2"> <label>Parent</label> <select class="border p-1 w-full" name="parent_id"> <option value="">—</option> @foreach($parents as $p) <option value="{{ $p->id }}" @selected($item->parent_id==$p->id)>{{ $p->label }}</option> @endforeach </select> </div> <div class="mb-2"> <label>Route name</label> <input class="border p-1 w-full" name="route_name" value="{{ old('route_name',$item->route_name) }}"> </div> <div class="mb-2"> <label>URL</label> <input class="border p-1 w-full" name="url" value="{{ old('url',$item->url) }}"> </div> <div class="mb-2"> <label>Icon</label> <input class="border p-1 w-full" name="icon" value="{{ old('icon',$item->icon) }}"> </div> <div class="mb-2"> <label>Descrizione</label> <input class="border p-1 w-full" name="description" value="{{ old('description',$item->description) }}"> </div> <div class="mb-2"> <label>Ordine</label> <input class="border p-1 w-full" name="order_index" type="number" value="{{ old('order_index',$item->order_index) }}"> </div> <div class="mb-2"> <label>Ruoli richiesti (comma separated)</label> <input class="border p-1 w-full" name="required_roles" value="{{ old('required_roles', $item->required_roles ? implode(',',$item->required_roles) : '') }}"> </div> <div class="mb-2"> <label>Visibile?</label> <input type="checkbox" name="is_visible" @checked($item->is_visible)> </div> <button class="px-3 py-1 bg-blue-600 text-white rounded">Aggiorna</button> </form> @endsection `

    ---

    7) Menu dinamico (seed iniziale con “Servizio”)

    `php $service = \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Servizio' ],[ 'icon'=>'lucide-wrench', 'order_index'=>100, 'required_roles'=>json_encode(['super-admin']) ]);

    \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>$service->id,'label'=>'Configurazione menù' ],[ 'route_name'=>'admin.systems.menu.index', 'order_index'=>1, 'required_roles'=>json_encode(['super-admin']) ]); `

    ---

    8) Sicurezza accessi

  • Super Admin: bypass via Gate::before + middleware role:super-admin sulle rotte “Servizio”.
  • Ruolo attivo in sessione: EnsureActiveRole garantisce selezione ruolo.
  • Menu filtering: MenuService filtra per required_roles e required_permissions.
  • Audit (futuro): log policy per CRUD delle voci menu.
  • ---

    9) Criteri di accettazione

  • Login/logout/reset funzionanti.
  • Selettore ruolo attivo post-login (+ badge ruolo in header).
  • Topbar & Sidebar generati da DB (MenuService).
  • Servizio (super-admin) in topbar; prima voce = Configurazione menù CRUD funzionante.
  • Voce Industria 4.0 → Report → Macchine → Elenco macchine collegate presente (placeholder).
  • ---

    10) TODO per Cursor

  • [ ] Creare routes/auth.php e routes/admin.php + includerli in RouteServiceProvider
  • [ ] Implementare controller come da namespace/cartelle
  • [ ] Migrazioni e seeders (Menu, MenuItem, Ruoli, SuperAdmin) con “Servizio”
  • [ ] MenuService + helper menu_is_active()
  • [ ] Layout Blade layouts/admin.blade.php + views sistemi/menu (index/create/edit)
  • [ ] Middleware EnsureActiveRole registrato in Kernel
  • [ ] Gate super-admin in AuthServiceProvider`
  • [ ] Seed “Servizio” con “Configurazione menù” come prima voce

Analisi Codice

Blocco 1
app/
  Http/
    Controllers/
      Admin/
        Dashboard/
          DashboardController.php
        I40/
          HomeController.php
          Machines/
            MachinesController.php
        Systems/            # "Servizio" (super-admin only)
          Menu/
            MenuController.php
          Users/
            UsersController.php
          Roles/
            RolesController.php
    Middleware/
      EnsureActiveRole.php
  Models/
    Menu.php
    MenuItem.php
  Policies/
    MenuPolicy.php
app/
  Services/
    MenuService.php
resources/
  views/
    layouts/
      admin.blade.php
      auth.blade.php
    auth/
      login.blade.php
      select-role.blade.php
      forgot.blade.php
      reset.blade.php
    admin/
      dashboard/index.blade.php
      i40/home.blade.php
      i40/machines/connected.blade.php
      systems/                 # Servizio (Super Admin)
        index.blade.php
        menu/
          index.blade.php      # lista voci
          create.blade.php
          edit.blade.php
routes/
  admin.php
  auth.php
database/
  migrations/
    2025_xx_xx_create_menus_table.php
    2025_xx_xx_create_menu_items_table.php
  seeders/
    RolesSeeder.php
    SuperAdminSeeder.php
    MenuSeeder.php
Blocco 2 php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\ResetPasswordController;
use App\Http\Controllers\Auth\RoleSelectorController;

Route::middleware('guest')->group(function () {
  Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
  Route::post('/login', [LoginController::class, 'login'])->name('login.post');
  Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
  Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
  Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
  Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');
});

Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');

Route::middleware('auth')->group(function () {
  Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select');
  Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set');
});
Blocco 3 php
<?php

use Illuminate\Support\Facades\Route;

Route::middleware(['auth','active.role'])->prefix('admin')->name('admin.')->group(function () {

  // Dashboard
  Route::get('/', [\App\Http\Controllers\Admin\Dashboard\DashboardController::class,'index'])
    ->name('dashboard');

  // Industria 4.0
  Route::prefix('i40')->name('i40.')->group(function() {
    Route::get('/', [\App\Http\Controllers\Admin\I40\HomeController::class,'index'])->name('home');
    Route::get('/machines/connected', [\App\Http\Controllers\Admin\I40\Machines\MachinesController::class,'connected'])
      ->name('machines.connected');
  });

  // Servizio (Super Admin menu)
  Route::prefix('systems')->name('systems.')->middleware('role:super-admin')->group(function () {
    Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('home');

    // Configurazione menù (prima voce)
    Route::prefix('menu')->name('menu.')->group(function () {
      Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('index');
      Route::get('/create', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'create'])->name('create');
      Route::post('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'store'])->name('store');
      Route::get('/{item}/edit', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'edit'])->name('edit');
      Route::put('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'update'])->name('update');
      Route::delete('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'destroy'])->name('destroy');
      Route::post('/reorder', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'reorder'])->name('reorder');
    });
  });

});
Blocco 4 php
<?php
namespace App\Http\Controllers\Admin\Dashboard;

use App\Http\Controllers\Controller;

class DashboardController extends Controller
{
  public function index() {
    return view('admin.dashboard.index');
  }
}
Blocco 5 php
<?php
namespace App\Http\Controllers\Admin\I40\Machines;

use App\Http\Controllers\Controller;

class MachinesController extends Controller
{
  public function connected() {
    return view('admin.i40.machines.connected');
  }
}
Blocco 6 php
<?php
namespace App\Http\Controllers\Admin\Systems\Menu;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Menu;
use App\Models\MenuItem;

class MenuController extends Controller
{
  public function index() {
    $menu = Menu::where('name','admin_main')->first();
    $items = $menu ? $menu->items()->orderBy('order_index')->get() : collect();
    return view('admin.systems.menu.index', compact('menu','items'));
  }

  public function create() {
    $menu = Menu::where('name','admin_main')->first();
    $parents = $menu?->items()->orderBy('label')->get();
    return view('admin.systems.menu.create', compact('menu','parents'));
  }

  public function store(Request $r) {
    $r->validate([
      'label'=>'required|string|max:100',
      'parent_id'=>'nullable|integer',
      'route_name'=>'nullable|string|max:120',
      'url'=>'nullable|url',
      'order_index'=>'nullable|integer',
    ]);
    $menu = Menu::firstOrCreate(['name'=>'admin_main']);
    MenuItem::create([
      'menu_id'=>$menu->id,
      'parent_id'=>$r->parent_id,
      'label'=>$r->label,
      'route_name'=>$r->route_name ?: null,
      'url'=>$r->url ?: null,
      'icon'=>$r->icon,
      'description'=>$r->description,
      'order_index'=>$r->order_index ?? 0,
      'is_visible'=>$r->boolean('is_visible', true),
      'required_roles'=>$r->filled('required_roles') ? array_values(array_filter(array_map('trim', explode(',', $r->required_roles)))) : null,
      'required_permissions'=>$r->filled('required_permissions') ? array_values(array_filter(array_map('trim', explode(',', $r->required_permissions)))) : null,
    ]);
    return redirect()->route('admin.systems.menu.index')->with('ok','Voce aggiunta');
  }

  public function edit(MenuItem $item) {
    $menu = Menu::where('name','admin_main')->first();
    $parents = $menu?->items()->where('id','!=',$item->id)->orderBy('label')->get();
    return view('admin.systems.menu.edit', compact('item','parents'));
  }

  public function update(Request $r, MenuItem $item) {
    $r->validate([
      'label'=>'required|string|max:100',
      'parent_id'=>'nullable|integer',
      'route_name'=>'nullable|string|max:120',
      'url'=>'nullable|url',
      'order_index'=>'nullable|integer',
    ]);
    $item->update([
      'parent_id'=>$r->parent_id,
      'label'=>$r->label,
      'route_name'=>$r->route_name ?: null,
      'url'=>$r->url ?: null,
      'icon'=>$r->icon,
      'description'=>$r->description,
      'order_index'=>$r->order_index ?? 0,
      'is_visible'=>$r->boolean('is_visible', true),
      'required_roles'=>$r->filled('required_roles') ? array_values(array_filter(array_map('trim', explode(',', $r->required_roles)))) : null,
      'required_permissions'=>$r->filled('required_permissions') ? array_values(array_filter(array_map('trim', explode(',', $r->required_permissions)))) : null,
    ]);
    return redirect()->route('admin.systems.menu.index')->with('ok','Voce aggiornata');
  }

  public function destroy(MenuItem $item) {
    $item->delete();
    return back()->with('ok','Voce rimossa');
  }

  public function reorder(Request $r) {
    foreach ($r->input('items',[]) as $row) {
      \App\Models\MenuItem::whereKey($row['id'])->update(['order_index'=>$row['order_index']]);
    }
    return response()->json(['ok'=>true]);
  }
}
Blocco 7 php
<?php
// app/Models/Menu.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Menu extends Model {
  protected $fillable = ['name','description','is_active'];
  public function items() { return $this->hasMany(MenuItem::class); }
}
Blocco 8 php
<?php
// app/Models/MenuItem.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class MenuItem extends Model {
  protected $fillable = [
    'menu_id','parent_id','label','route_name','url','icon','description',
    'order_index','is_visible','required_roles','required_permissions'
  ];
  protected $casts = ['required_roles'=>'array','required_permissions'=>'array'];
  public function parent(){ return $this->belongsTo(MenuItem::class,'parent_id'); }
  public function children(){ return $this->hasMany(MenuItem::class,'parent_id'); }
  public function menu(){ return $this->belongsTo(Menu::class); }
}
Blocco 9 php
<?php
// app/Policies/MenuPolicy.php
namespace App\Policies;
use App\Models\User;
use App\Models\MenuItem;

class MenuPolicy
{
  public function manage(User $user) {
    return $user->hasRole('super-admin');
  }
  public function view(User $user, MenuItem $item) {
    if ($user->hasRole('super-admin')) return true;
    $activeRole = session('active_role');
    if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false;
    if ($item->required_permissions) {
      foreach ($item->required_permissions as $perm) {
        if (!$user->can($perm)) return false;
      }
    }
    return true;
  }
}
Blocco 10 php
<?php
// app/Services/MenuService.php
namespace App\Services;
use App\Models\Menu;
use App\Models\MenuItem;
use App\Models\User;

class MenuService {
  public function forUserMenu(string $menuName, ?User $user): array {
    $menu = Menu::where('name',$menuName)->first();
    if (!$menu) return [];
    $items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id');
    $activeRole = session('active_role');

    $filter = function(MenuItem $item) use ($user,$activeRole) {
      if (!$item->is_visible) return false;
      if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false;
      if ($item->required_permissions) {
        foreach ($item->required_permissions as $perm) {
          if (!$user || !$user->can($perm)) return false;
        }
      }
      return true;
    };

    $build = function($parentId) use (&$build, $items, $filter) {
      return ($items[$parentId] ?? collect())->filter($filter)->map(function($i) use (&$build) {
        return [
          'id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'icon'=>$i->icon,
          'children'=>$build($i->id)->values()->all()
        ];
      });
    };

    return $build(null)->values()->all();
  }
}
Blocco 11 php
<?php
// app/helpers.php
if (!function_exists('menu_is_active')) {
  function menu_is_active(?string $routeName): bool {
    if (!$routeName) return false;
    return request()->routeIs($routeName) || request()->routeIs($routeName.'.*');
  }
}
Blocco 12 json
"autoload": {
  "psr-4": {
    "App\\": "app/"
  },
  "files": [
    "app/helpers.php"
  ]
}
Blocco 13 blade
{{-- resources/views/layouts/admin.blade.php --}}
@php($menu = app(\App\Services\MenuService::class)->forUserMenu('admin_main', auth()->user()))
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>@yield('title','sartUP Admin')</title>
  @vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
  <header class="h-14 shadow flex items-center px-4 bg-white">
    <nav class="flex gap-4">
      @foreach($menu as $item)
        <a class="font-medium {{ menu_is_active($item['route_name']) ? 'text-blue-600' : '' }}"
           href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}">
          {{ $item['label'] }}
        </a>
      @endforeach
      @role('super-admin')
        <a class="font-medium {{ request()->is('admin/systems*') ? 'text-blue-600' : '' }}"
           href="{{ route('admin.systems.menu.index') }}">Servizio</a>
      @endrole
    </nav>
    <div class="ml-auto flex items-center gap-2">
      @if(session('active_role'))
        <span class="text-xs px-2 py-1 bg-gray-200 rounded">Ruolo: {{ session('active_role') }}</span>
      @endif
      <form method="POST" action="{{ route('logout') }}">@csrf<button>Logout</button></form>
    </div>
  </header>
  <div class="flex">
    <aside class="w-64 bg-white border-r min-h-[calc(100vh-3.5rem)] p-3">
      @php($first = $menu[0] ?? null)
      @if($first && count($first['children']))
        @foreach($first['children'] as $child)
          <div class="mb-3">
            <div class="font-semibold">{{ $child['label'] }}</div>
            @if(count($child['children']))
              <ul class="ml-3 list-disc">
                @foreach($child['children'] as $sub)
                  <li>
                    <a href="{{ $sub['route_name'] ? route($sub['route_name']) : ($sub['url'] ?? '#') }}">
                      {{ $sub['label'] }}
                    </a>
                  </li>
                @endforeach
              </ul>
            @endif
          </div>
        @endforeach
      @endif
    </aside>
    <main class="flex-1 p-6">
      @if(session('ok')) <div class="mb-4 p-2 bg-green-100">{{ session('ok') }}</div> @endif
      @yield('content')
    </main>
  </div>
</body>
</html>
Blocco 14 blade
{{-- resources/views/admin/systems/index.blade.php --}}
@extends('layouts.admin')
@section('title','Servizio')
@section('content')
  <h1 class="text-xl font-semibold mb-4">Servizio</h1>
  <ul class="list-disc ml-6">
    <li><a href="{{ route('admin.systems.menu.index') }}">Configurazione menù</a></li>
  </ul>
@endsection
Blocco 15 blade
{{-- resources/views/admin/systems/menu/index.blade.php --}}
@extends('layouts.admin')
@section('title','Configurazione menù')
@section('content')
  <h1 class="text-xl font-semibold mb-4">Configurazione menù</h1>

  <a href="{{ route('admin.systems.menu.create') }}" class="underline">Nuova voce</a>

  <table class="min-w-full bg-white shadow border mt-4">
    <thead><tr>
      <th class="p-2 text-left">Label</th>
      <th class="p-2 text-left">Parent</th>
      <th class="p-2 text-left">Route</th>
      <th class="p-2 text-left">URL</th>
      <th class="p-2 text-left">Roles</th>
      <th class="p-2 text-left">Ordine</th>
      <th class="p-2"></th>
    </tr></thead>
    <tbody>
      @forelse($items as $i)
        <tr>
          <td class="p-2">{{ $i->label }}</td>
          <td class="p-2">{{ $i->parent?->label ?? '—' }}</td>
          <td class="p-2">{{ $i->route_name ?? '—' }}</td>
          <td class="p-2">{{ $i->url ?? '—' }}</td>
          <td class="p-2">{{ $i->required_roles ? implode(',', $i->required_roles) : '—' }}</td>
          <td class="p-2">{{ $i->order_index }}</td>
          <td class="p-2">
            <a class="underline" href="{{ route('admin.systems.menu.edit',$i) }}">Modifica</a>
            <form method="POST" action="{{ route('admin.systems.menu.destroy',$i) }}" class="inline">
              @csrf @method('DELETE')
              <button class="underline text-red-600" onclick="return confirm('Eliminare?')">Elimina</button>
            </form>
          </td>
        </tr>
      @empty
        <tr><td class="p-2" colspan="7">Nessuna voce ancora.</td></tr>
      @endforelse
    </tbody>
  </table>
@endsection
Blocco 16 blade
{{-- resources/views/admin/systems/menu/create.blade.php --}}
@extends('layouts.admin')
@section('title','Nuova voce menù')
@section('content')
  <h1 class="text-xl font-semibold mb-4">Nuova voce</h1>
  <form method="POST" action="{{ route('admin.systems.menu.store') }}">
    @csrf
    <div class="mb-2">
      <label>Label</label>
      <input class="border p-1 w-full" name="label" required>
    </div>
    <div class="mb-2">
      <label>Parent</label>
      <select class="border p-1 w-full" name="parent_id">
        <option value="">—</option>
        @foreach($parents as $p)
          <option value="{{ $p->id }}">{{ $p->label }}</option>
        @endforeach
      </select>
    </div>
    <div class="mb-2">
      <label>Route name</label>
      <input class="border p-1 w-full" name="route_name">
    </div>
    <div class="mb-2">
      <label>URL</label>
      <input class="border p-1 w-full" name="url" placeholder="http(s)://…">
    </div>
    <div class="mb-2">
      <label>Icon</label>
      <input class="border p-1 w-full" name="icon">
    </div>
    <div class="mb-2">
      <label>Descrizione</label>
      <input class="border p-1 w-full" name="description">
    </div>
    <div class="mb-2">
      <label>Ordine</label>
      <input class="border p-1 w-full" name="order_index" type="number" value="0">
    </div>
    <div class="mb-2">
      <label>Ruoli richiesti (comma separated)</label>
      <input class="border p-1 w-full" name="required_roles" placeholder="admin,maintenance">
    </div>
    <div class="mb-2">
      <label>Visibile?</label>
      <input type="checkbox" name="is_visible" checked>
    </div>
    <button class="px-3 py-1 bg-blue-600 text-white rounded">Salva</button>
  </form>
@endsection
Blocco 17 blade
{{-- resources/views/admin/systems/menu/edit.blade.php --}}
@extends('layouts.admin')
@section('title','Modifica voce menù')
@section('content')
  <h1 class="text-xl font-semibold mb-4">Modifica voce</h1>
  <form method="POST" action="{{ route('admin.systems.menu.update',$item) }}">
    @csrf @method('PUT')
    <div class="mb-2">
      <label>Label</label>
      <input class="border p-1 w-full" name="label" required value="{{ old('label',$item->label) }}">
    </div>
    <div class="mb-2">
      <label>Parent</label>
      <select class="border p-1 w-full" name="parent_id">
        <option value="">—</option>
        @foreach($parents as $p)
          <option value="{{ $p->id }}" @selected($item->parent_id==$p->id)>{{ $p->label }}</option>
        @endforeach
      </select>
    </div>
    <div class="mb-2">
      <label>Route name</label>
      <input class="border p-1 w-full" name="route_name" value="{{ old('route_name',$item->route_name) }}">
    </div>
    <div class="mb-2">
      <label>URL</label>
      <input class="border p-1 w-full" name="url" value="{{ old('url',$item->url) }}">
    </div>
    <div class="mb-2">
      <label>Icon</label>
      <input class="border p-1 w-full" name="icon" value="{{ old('icon',$item->icon) }}">
    </div>
    <div class="mb-2">
      <label>Descrizione</label>
      <input class="border p-1 w-full" name="description" value="{{ old('description',$item->description) }}">
    </div>
    <div class="mb-2">
      <label>Ordine</label>
      <input class="border p-1 w-full" name="order_index" type="number" value="{{ old('order_index',$item->order_index) }}">
    </div>
    <div class="mb-2">
      <label>Ruoli richiesti (comma separated)</label>
      <input class="border p-1 w-full" name="required_roles" value="{{ old('required_roles', $item->required_roles ? implode(',',$item->required_roles) : '') }}">
    </div>
    <div class="mb-2">
      <label>Visibile?</label>
      <input type="checkbox" name="is_visible" @checked($item->is_visible)>
    </div>
    <button class="px-3 py-1 bg-blue-600 text-white rounded">Aggiorna</button>
  </form>
@endsection
Blocco 18 php
$service = \App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Servizio'
],[
  'icon'=>'lucide-wrench',
  'order_index'=>100,
  'required_roles'=>json_encode(['super-admin'])
]);

\App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>$service->id,'label'=>'Configurazione menù'
],[
  'route_name'=>'admin.systems.menu.index',
  'order_index'=>1,
  'required_roles'=>json_encode(['super-admin'])
]);